﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Xna.Framework;
using System.Diagnostics;

namespace Project
{
    public class CollisionMap
    {
        static void initTileSet()
        {
            tileSet = new Dictionary<int, MapTile>();
            // 0 = air
            tileSet[0] = new MapTile(false, delegate(CollisionData data) {});
            // 1 = solid block
            tileSet[1] = new MapTile(true, delegate(CollisionData data) {});
            // 9 = checkpoint
            // TODO checkpoint stuff
            tileSet[9] = new MapTile(true, delegate(CollisionData data) {
                if (data.entity is Player)
                {
                    Player p = (Player) data.entity;
                    p.setCheckpoint();
                }
            });
            // 11 = instant-death wall 
            tileSet[11] = new MapTile(true, delegate(CollisionData data)
            {
                data.entity.damage(1000f);
            });
            // 41 = goal block
            tileSet[41] = new MapTile(false, delegate(CollisionData data) { if (data.entity is Player) { data.gameState.onWin(); } });
            // 46 = spiky launcher (treat like air)
            tileSet[46] = new MapTile(false, delegate(CollisionData data) {});
        }

        private static Dictionary<int, MapTile> tileSet;

        protected int width { get; private set; }
        protected int height { get; private set; }
        // 2-dimensional array of RLE compressed MapTile data
        // first int is an index into the tileSet
        // second int is the number of consecutive tiles of this type
        protected List<LinkedList<Tuple<int, int>>> data { get; private set; }

        public static CollisionMap loadFromFile(StreamReader sr)
        {
            if (tileSet == null)
            {
                initTileSet();
            }

            CollisionMap ret = new CollisionMap();

            // StreamReader sr = new StreamReader(stream);
            string[] dims = sr.ReadLine().Split(" ".ToCharArray());
            ret.width = int.Parse(dims[0]);
            ret.height = int.Parse(dims[1]);

            string rleData;
            ret.data = new List<LinkedList<Tuple<int, int>>>();
            while ((rleData = sr.ReadLine()) != null && rleData.Length > 0)
            {
                LinkedList<Tuple<int, int>> column = new LinkedList<Tuple<int, int>>();
                string[] ints = rleData.Split(" ".ToCharArray());
                for (int i = 0; i < ints.Length - 1; i += 2)
                {
                    int tileType = int.Parse(ints[i]);
                    int tileSpan = int.Parse(ints[i + 1]);
                    column.AddLast(new Tuple<int, int>(tileType, tileSpan));
                }
                ret.data.Add(column);
            }
            return ret;
        }

        /// <summary>
        /// Returns an iterator of all MapTile objects within the specified bounds.
        /// </summary>
        /// <param name="bounds">The axis-aligned bounding box to use</param>
        /// <returns>An iterator of all MapTiles within the given bounds</returns>
        public IEnumerable<MapTile> getTiles(AABB aabb)
        {
            AABB adjustedBounds = aabb;
            adjustedBounds.position *= -1;
            // loop over all x, y pairs of possibly-colliding columns
            Vector3 min = adjustedBounds.WorldMin;
            Vector3 max = adjustedBounds.WorldMax;
            for (float x = (float)Math.Max(min.X, 0.0); x < max.X + 1.1f && x < width; x += 1.0f)
            {
                for (float z = (float)Math.Max(min.Z, 0.0); z < max.Z + 1.1f && z < height; z += 1.0f)
                {
                    int xQuery = (int)Math.Min(x, max.X);
                    int zQuery = (int)Math.Min(z, max.Z);
                    LinkedList<Tuple<int, int>> col = data[xQuery * width + zQuery];
                    int vert = 0;
                    foreach (Tuple<int, int> tileSpan in col)
                    {
                        int tileId = tileSpan.Item1;
                        int startVert = vert;
                        int endVert = vert + tileSpan.Item2;

                        if (startVert < max.Y && endVert > min.Y)
                        {
                            yield return tileSet[tileId];
                        }
                        else if (startVert > max.Y)
                        {
                            break;
                        }
                        vert = endVert;
                    }
                }
            }
        }

        /// <summary>
        /// Attempts to nudge the provided bounding box to a location where it does not intersect solid geometry.
        /// </summary>
        /// <param name="bounds"></param>
        /// <returns></returns>
        public bool nudgeAABB(ref AABB bounds)
        {
            if (!collides(bounds))
            {
                return true;
            }
            Vector3 span = bounds.max - bounds.min;
            // potentially-slow brute-force method:
            // try all new AABB positions within the original box
            Vector3 candidatePos = bounds.position;
            float minSqDist = -1;
            for (float x = bounds.min.X; x < bounds.max.X; x++)
            {
                for (float z = bounds.min.Z; z < bounds.max.Z; z++)
                {
                    for (float y = bounds.min.Y; y < bounds.max.Y; y++)
                    {
                        AABB test = bounds;
                        test.position = bounds.position + new Vector3(x, y, z);
                        if (!collides(test))
                        {
                            float sqDist = Vector3.DistanceSquared(test.position, bounds.position);
                            if (sqDist < minSqDist || minSqDist == -1)
                            {
                                minSqDist = sqDist;
                                candidatePos = test.position;
                            }
                        }
                    }
                }
            }
            if (minSqDist == -1)
                return false;
            bounds.position = candidatePos;
            return true;
        }

        /// <summary>
        /// </summary>
        /// <param name="c">The axis aligned bounding box to test.</param>
        /// <returns>True if there are solid tiles within the bounds specified, false otherwise</returns>
        public bool collides(AABB c)
        {
            foreach (MapTile tile in getTiles(c))
            {
                if (tile.isSolid)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Uses the specified collision object to interact with static map entities.
        /// The appropriate collision handler for each map tile intersecting the object is invoked.
        /// </summary>
        /// <param name="c"></param>
        public void interact(CollisionObject c, GameTime gt, GameState gs)
        {
            foreach (MapTile tile in getTiles(c.collisionBox))
            {
                tile.collisionHandler(new CollisionData(c.parent, gt, gs));
            }
        }
    }

    public struct MapTile
    {
        public bool isSolid;
        public CollisionObject.CollisionHandler collisionHandler;

        public MapTile(bool isSolid, CollisionObject.CollisionHandler handler)
        {
            this.isSolid = isSolid;
            this.collisionHandler = handler;
        }
    }
}
